Skip to content

feat(conversations): export conversations as Markdown and PDF - #531

Merged
ewen-poch merged 2 commits into
aipoch:mainfrom
EuanTop:feat/conversation-export
Jul 31, 2026
Merged

feat(conversations): export conversations as Markdown and PDF#531
ewen-poch merged 2 commits into
aipoch:mainfrom
EuanTop:feat/conversation-export

Conversation

@EuanTop

@EuanTop EuanTop commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Problem

Conversation content can currently be downloaded one item at a time, but users cannot export a complete conversation as a portable document. This makes it difficult to archive, share, or reuse a finished research session outside Open Science.

Addresses #486.

Proposed change

  • Add whole-conversation export actions for Markdown and PDF to the conversation sidebar menu.
  • Project persisted session data into a normalized public export model rather than exposing the internal session JSON.
  • Render Markdown from the normalized model and render PDF through an isolated hidden Electron window.
  • Reload the durable session in the main process before export so the output is based on persisted state.
  • Restrict generated PDF HTML with a CSP and escaped/sanitized content, without loading remote resources.
  • Preserve complete prompt-derived titles in document content and default filenames whenever the filesystem permits it.
  • Bound default filenames by UTF-8 byte length, preserving complete Unicode characters and leaving room for the extension.
  • Disable export for empty, running, and permission-waiting sessions.
  • Keep export unavailable in the web runtime because the desktop Save As capability is intentionally absent there.

Scope and non-goals

  • PDF and Markdown export are included.
  • PNG export is intentionally not included in this PR.
  • Persisted session JSON is not exposed directly to users.
  • The existing session persistence format is unchanged.
  • No generated PDF or Markdown artifacts are committed.
  • Web export is not added; this remains a desktop-only capability.

Acceptance criteria and validation

  • A completed, non-empty conversation can be exported as a readable Markdown file.
  • The same conversation can be exported as a paginated PDF with long titles rendered correctly.
  • Save As uses the complete sanitized conversation title unless it approaches the portable filename byte limit.
  • Filename truncation does not split Unicode characters and is visibly marked with an ellipsis.
  • Internal metadata, local paths, and thinking content are omitted from public exports.
  • Empty, running, and permission-waiting sessions do not offer an active export action.
  • Temporary PDF resources and the hidden renderer window are cleaned up after success or failure.
  • Desktop IPC is wired through preload without exposing the capability through the web API map.

All checks below ran after the last material edit, including merge-conflict resolution commit c228f82:

  • Export projection, Markdown/PDF rendering, filename handling, temporary-resource cleanup, IPC forwarding, desktop/web capability filtering, sidebar actions, and collapsible-panel integration -> npm test -- --run src/shared/conversation-export.test.ts src/main/session-persistence/conversation-export.test.ts src/main/web-service/http-server.test.ts src/preload/index.test.ts src/renderer/web/api-map.generated.test.ts src/renderer/src/pages/workspace/WorkspaceSidebar.render.test.tsx src/renderer/src/pages/workspace/WorkspacePage.draft-preservation.test.tsx src/renderer/src/pages/workspace/WorkspacePage.preview-panel-resize.test.tsx -> passed, 8 files and 126 tests.
  • Node and renderer type safety -> npm run typecheck -> passed.
  • Repository lint rules -> npm run lint -> passed with 0 errors; 23 existing warnings are outside this PR diff.
  • Full regression suite -> npm test -> passed, 574 files passed and 11 skipped; 8,598 tests passed and 139 skipped.
  • Generated web API synchronization -> npm run check:web-api-map -> passed.
  • Changed-file formatting -> npx prettier --check package.json package-lock.json src/main/ipc.ts src/main/session-persistence/conversation-export.test.ts src/main/session-persistence/conversation-export.ts src/main/web-service/http-server.test.ts src/main/web-service/http-server.ts src/preload/index.d.ts src/preload/index.test.ts src/preload/index.ts src/renderer/src/pages/workspace/WorkspacePage.tsx src/renderer/src/pages/workspace/WorkspaceSidebar.render.test.tsx src/renderer/src/pages/workspace/WorkspaceSidebar.tsx src/renderer/web/api-map.generated.ts src/shared/conversation-export.test.ts src/shared/conversation-export.ts -> passed.
  • Conflict-marker and whitespace validation -> git diff --check origin/main HEAD -> passed.

Uncovered risks: this conflict-resolution pass did not repeat manual native Save As/PDF verification on every supported operating system. The previously posted review findings about active-session status normalization, unmatched reasoning blocks, and prompt-derived document-title truncation remain open for follow-up.

Review focus

  • The public export projection and removal of internal metadata.
  • HTML escaping, Markdown rendering, CSP, and remote-content restrictions.
  • Filename sanitization and UTF-8 byte-boundary behavior.
  • Cleanup of the temporary PDF directory and hidden renderer window.
  • The desktop/web capability boundary across IPC, preload, and the generated web API map.
  • Sidebar enablement rules and export submenu behavior.

Copilot AI review requested due to automatic review settings July 30, 2026 15:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added the enhancement New feature or request label Jul 30, 2026
@EuanTop
EuanTop force-pushed the feat/conversation-export branch from 21c1830 to 7ce0c2b Compare July 30, 2026 15:06
Copilot AI review requested due to automatic review settings July 30, 2026 15:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@EuanTop
EuanTop force-pushed the feat/conversation-export branch from 7ce0c2b to 48ff86f Compare July 30, 2026 15:25
Copilot AI review requested due to automatic review settings July 30, 2026 15:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@EuanTop
EuanTop marked this pull request as ready for review July 30, 2026 15:31

@ewen-poch ewen-poch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the thoughtful, security-conscious implementation and the extensive test coverage. The normalized public export model, isolated PDF renderer, and desktop/web boundary are all valuable additions.

I found the following issues that should be addressed before merge:

  1. The production active-session guard is ineffective. In src/main/session-persistence/conversation-export.ts:95-99, the service checks the status returned by loadSession. In production this is SessionRepository.loadSession(), which goes through normalizeSessionFile() -> normalizeSessionAfterRestore() and converts persisted running and waiting-permission sessions to error before returning them. As a result, a stale renderer action or IPC call can export an incomplete active conversation, even though the mocked service test passes. Please check an authoritative live status or use a durable read that does not apply restart normalization, and add an integration test using the real repository.

  2. An unfinished reasoning block can leak into a public export. sanitizeExportMarkdown removes only closed <think>...</think> pairs, and the new test explicitly expects before <think>unfinished to remain. This conflicts with the acceptance criterion that thinking content is omitted, especially for errored/interrupted responses. Please strip an unmatched assistant <think> opener through EOF as well, while continuing to preserve user-authored examples.

  3. Prompt-derived document titles are still truncated. deriveTitleFromPrompt keeps only the first non-empty line/first sentence and applies a 240-character cap before the value is rendered into Markdown/PDF. The requirement says complete prompt-derived titles should be preserved in document content; only the filename needs a filesystem byte limit. Please keep the full public document title and apply truncation only in sanitizeExportFilename.

There are also two merge-readiness items:

  • CONTRIBUTING.md requires a final behavior -> exact command -> result evidence mapping, confirmation that checks ran after the last material edit, and uncovered risks. The current validation section lists aggregate results but does not provide that mapping.
  • The PR currently conflicts with the latest main (including the recent artifact-download changes), so it needs to be rebased and the sidebar/web-channel conflicts reconciled.

One smaller acceptance-wording mismatch: the generated web API map contains sessions.exportConversation; the HTTP denylist does correctly keep the capability unavailable at runtime. Please either adjust the stated criterion to describe runtime capability filtering or update the map-generation boundary if the map itself must exclude the method.

Resolve conflicts with collapsible workspace panels and selective artifact downloads while preserving desktop-only conversation export.
Copilot AI review requested due to automatic review settings July 31, 2026 04:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

@ewen-poch
ewen-poch merged commit 0140f32 into aipoch:main Jul 31, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants